namespace ExpressIS.NetduinoDeviceLibrary {
  using System;
  using Microsoft.SPOT.Hardware;

  /// <summary>
  /// DS1307 (Sparkfun Breakout Board) - RTC
  /// 
  /// Use this breakout board from 5V.
  /// </summary>
  public sealed class DS1307 : I2CDeviceHelper {
    // Real time clock I2C address
    private const ushort HARDWARE_BASE_ADDRESS = 0x68;

    // Total size of the user RAM block
    private const byte DS1307_RAM_SIZE = 56;

    // Start / End addresses of the user RAM registers
    private const byte DS1307_RAM_START_ADDRESS = 0x08;
    private const byte DS1307_RAM_END_ADDRESS = 0x3f;

    // Start / End addresses of the date/time registers
    private const byte DS1307_RTC_START_ADDRESS = 0x00;
    private const byte DS1307_RTC_END_ADDRESS = 0x06;

    // Square wave frequency generator register address
    private const byte DS1307_SQUARE_WAVE_CTRL_REGISTER_ADDRESS = 0x07;

    // Defines the frequency of the signal on the SQW interrupt pin on the clock when enabled
    public enum SquareWaveFrequency { SQW_1Hz, SQW_4kHz, SQW_8kHz, SQW_32kHz, SQW_Off };

    // Defines the logic level on the SQW pin when the frequency is disabled
    public enum SquareWaveDisabledOutputControl { Zero, One };

    public DS1307(I2CDevice sharedI2C, int clockRateKHz = 100, int transacitonTimeoutMs = 20, ushort hardwareAddress = HARDWARE_BASE_ADDRESS)
      : base(sharedI2C, hardwareAddress, clockRateKHz, transacitonTimeoutMs) { }

    protected override void Dispose(bool disposing) {
      base._disposed = true;
    }

    public byte[] NonVolatileRam {
      get {
        byte[] result = new byte[0];
        this.SwapConfiguration(() =>
        {
          result = new byte[DS1307_RAM_SIZE];

          var transaction = new I2CDevice.I2CTransaction[] {
            I2CDevice.CreateWriteTransaction(new byte[] { DS1307_RAM_START_ADDRESS }),
            I2CDevice.CreateReadTransaction(result) 
          };

          if (this._sharedI2C.Execute(transaction, this._transactionTimeoutMs) == 0) {
            throw new Exception("I2C transaction failed");
          }
        });
        return result;
      }
      set {
        this.SwapConfiguration(() =>
        {
          if ((value == null) || (value.Length != DS1307_RAM_SIZE)) {
            throw new ArgumentOutOfRangeException("Invalid buffer length");
          }

          // Allocate a new buffer large enough to include the RAM start address byte and the payload
          var transactionBuffer = new byte[sizeof(byte) /*Address byte*/ + DS1307_RAM_SIZE];

          // Set the RAM start address
          transactionBuffer[0] = DS1307_RAM_START_ADDRESS;

          // Copy the user buffer after the address
          value.CopyTo(transactionBuffer, 1);

          // Write to the clock's RAM
          var transaction = new I2CDevice.I2CWriteTransaction[] { I2CDevice.CreateWriteTransaction(transactionBuffer) };

          if (this._sharedI2C.Execute(transaction, this._transactionTimeoutMs) == 0) {
            throw new Exception("I2C write transaction failed");
          }
        });
      }
    }

    public DateTime DateTime {
      get {
        // If we had true generic Func<T> and Action<T> delegates we would not need to do this local result.
        DateTime result = DateTime.MinValue;

        this.SwapConfiguration(() =>
        {
          byte[] clockData = new byte[7];

          // Read time registers (7 bytes from DS1307_RTC_START_ADDRESS)
          var transaction = new I2CDevice.I2CTransaction[] {
            I2CDevice.CreateWriteTransaction(new byte[] { DS1307_RTC_START_ADDRESS }),
            I2CDevice.CreateReadTransaction(clockData)
          };

          if (this._sharedI2C.Execute(transaction, this._transactionTimeoutMs) == 0) {
            throw new Exception("I2C transaction failed");
          }

          result = new DateTime(
            BcdToDec(clockData[6]) + 2000, // year
            BcdToDec(clockData[5]), // month
            BcdToDec(clockData[4]), // day
            BcdToDec(clockData[2] & 0x3f), // hours over 24 hours
            BcdToDec(clockData[1]), // minutes
            BcdToDec(clockData[0] & 0x7f) // seconds
          );
        });
        return result;
      }
      set {
        this.SwapConfiguration(() =>
        {
          var transaction = new I2CDevice.I2CWriteTransaction[] {
            I2CDevice.CreateWriteTransaction(new byte[] { 
              DS1307_RTC_START_ADDRESS, 
              DecToBcd(value.Second), 
              DecToBcd(value.Minute), 
              DecToBcd(value.Hour), 
              DecToBcd((int)value.DayOfWeek), 
              DecToBcd(value.Day), 
              DecToBcd(value.Month), 
              DecToBcd(value.Year - 2000)} )
          };

          if (this._sharedI2C.Execute(transaction, this._transactionTimeoutMs) == 0) {
            throw new Exception("I2C write transaction failed");
          }
        });
      }
    }

    public void SetSquareWave(SquareWaveFrequency squareWaveFrequency, SquareWaveDisabledOutputControl outputControl = SquareWaveDisabledOutputControl.Zero) {
      byte register = (byte)outputControl;

      register <<= 3;   // bit 7 defines the square wave output level when disabled
      // bit 6 & 5 are unused

      if (squareWaveFrequency != SquareWaveFrequency.SQW_Off) {
        register |= 1;
      }

      register <<= 4; // bit 4 defines if the oscillator generating the square wave frequency is on or off.
      // bit 3 & 2 are unused

      register |= (byte)squareWaveFrequency; // bit 1 & 0 define the frequency of the square wave

      this.SwapConfiguration(() =>
      {
        var transaction = new I2CDevice.I2CWriteTransaction[] {
                  I2CDevice.CreateWriteTransaction(new byte[] { DS1307_SQUARE_WAVE_CTRL_REGISTER_ADDRESS, register })
                };

        if (this._sharedI2C.Execute(transaction, this._transactionTimeoutMs) == 0) {
          throw new Exception("I2C write transaction failed");
        }
      });
    }

    public static DateTime GetLocalTimeFromRealTimeClock(I2CDevice sharedI2C) {
      using (var realTimeClock = new DS1307(sharedI2C, clockRateKHz: 100)) {
        // Get the userdata from the RTC storage (56 byts of data)
        byte[] realTimeClockUserData = realTimeClock.NonVolatileRam;

        // Get the timezone information from the first 2 bytes (number of minutes to offset)
        var timeZoneMinuteOffset = Utility.ExtractValueFromArray(realTimeClockUserData, 0, 2);

        // Get the time that is stored in the clock
        DateTime hardwareDateTime = realTimeClock.DateTime;

        // The time stored in the RTC is in GMT, so once we have it, we need to add our offset.
        return hardwareDateTime + new TimeSpan(TimeSpan.TicksPerMinute * (short)timeZoneMinuteOffset);
      }
    }
    public static TimeSpan GetLocalTimeZoneOffset(I2CDevice sharedI2C) {
      using (var realTimeClock = new DS1307(sharedI2C)) {
        // Get the userdata from the RTC storage (56 byts of data)
        byte[] realTimeClockUserData = realTimeClock.NonVolatileRam;

        // Get the timezone information (number of minutes to offset)
        var timeZoneMinuteOffset = Utility.ExtractValueFromArray(realTimeClockUserData, 0, 2);

        // return the new TimeSpan
        return new TimeSpan(TimeSpan.TicksPerMinute * (short)timeZoneMinuteOffset);
      }
    }

    public static void SetRealTimeClock(I2CDevice sharedI2C, DateTime utcDateTime) {
      using (var realTimeClock = new DS1307(sharedI2C)) {
        realTimeClock.DateTime = utcDateTime;
      }
    }
    public static void SetLocalTimeZoneOffset(I2CDevice sharedI2C, TimeSpan timeZoneOffset) {
      using (var realTimeClock = new DS1307(sharedI2C)) {
        // Get the userdata from the RTC storage (56 byts of data)
        byte[] realTimeClockUserData = realTimeClock.NonVolatileRam;

        // Set the timezone information (number of minutes to offset)
        Utility.InsertValueIntoArray(realTimeClockUserData, 0, 2, (uint)(timeZoneOffset.Ticks / TimeSpan.TicksPerMinute));

        // Update the stored data
        realTimeClock.NonVolatileRam = realTimeClockUserData;
      }
    }
  }
}
